#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <sys/wait.h>
#include "util.hpp"
#include "Log.hpp"
#include <unordered_map>
#include <sstream>
#include <algorithm>
#include <fcntl.h>



#define SEP ": " // 这个是将报头转化成kv结构时，字符串切割处的标识符
#define WEB_ROOT "wwwroot" // 这个是我们指定的资源默认请求路径
#define HOME_PAGE "index.html"
#define HTTP_VERSION "HTTP/1.0"
#define LINE_END "\r\n"
#define PAGE_404 "404.html"
#define PAGE_500 "500.html"
#define PAGE_400 "400.html"

#define OK 200
#define NOT_FOUND 404

#define OK 200
#define BAD_REQUEST 400 // 非法请求
#define NOT_FOUND 404 // 请求资源路径无效
#define SERVER_ERROR 500 // 服务器处理出错

// 状态码与状态码描述的映射，你往我这个函数里输入一个状态码，我就能返回给你该状态码对应的状态码描述
static std::string Code2Desc(int code)
{
    std::string desc;
    switch(code){
        case 200:
            desc = "OK";
            break;
        case 404:
            desc = "Not Found";
            break;
        default:
            break;
    }
    return desc;
}

// 后缀和文件类型之间的映射，你往我这个函数里输入一个文件名后缀，我就能给你返回该后缀对应的文件类型
static std::string Suffix2Desc(const std::string &suffix)
{
    static std::unordered_map<std::string, std::string> suffix2desc = {
        {".html", "text/html"},
        {".css", "text/css"},
        {".js", "application/javascript"},
        {".jpg", "application/x-jpg"},
        {".xml", "application/xml"},
    };

    auto iter = suffix2desc.find(suffix);
    if(iter != suffix2desc.end()){
        return iter->second;
    }
    // 如何查不到，我们默认该文件类型就是"text/html"
    return "text/html";
}
// 创建一个HTTP请求类，用于存储一份HTTP请求报文，将报文中各字段数据分别存储在类中的各成员变量中
class HttpRequest{
public:
    std::string request_line; // 存储HTTP请求报文的请求行
    std::vector<std::string> request_header;// 存储HTTP请求报文的请求报头
    std::string blank;  // 存储HTTP请求报文的空行
    std::string request_body; // 存储HTTP请求报文的正文
    //解析完毕之后的结果
    std::string method; // 请求方法
    std::string uri; //path?args
    std::string version;// 版本号

    std::unordered_map<std::string, std::string> header_kv; // 用map的形式存储请求报头中的信息
    int content_length; // 正文长度
    std::string path; // 资源请求路径
    std::string suffix; // 请求资源的类型/后缀
    std::string query_string; // 客户端上传文本内容
    int size; // 这个size表示http请求报文中所请求的资源的文件大小 （请求报文中url指向的文件的大小）
    

    bool cgi;// CGI模式的标志位，当用户向http服务器上传数据时，我们就需要使用CGI机制
    // 具体情况包括：POST请求、GET请求带参数、以及请求资源为exe文件

public:
    HttpRequest():content_length(0), cgi(false){}
    ~HttpRequest(){}
};
// 创建一个HTTP响应类，用于存储一份HTTP响应报文，将报文中各字段数据分别存储在类中的各成员变量中
class HttpResponse{
public:
    std::string status_line; // 存储HTTP响应报文的状态行
    std::vector<std::string> response_header;// 存储HTTP响应报文的响应报头
    std::string blank;// 存储HTTP响应报文的空行
    std::string response_body;// 存储HTTP响应报文的正文
    int status_code; // 状态码
    int fd;
    int size; // 这个size表示请求资源的文件大小 （请求报文中url指向的文件的大小）
public:
    HttpResponse():blank(LINE_END),status_code(OK),fd(-1){}
    ~HttpResponse(){}
};

//读取请求，分析请求，构建响应
///IO通信
class EndPoint{
private:
    int sock; // 前面调用accept返回的sock
    HttpRequest http_request;// 创建一个HTTP请求类的对象，用于存储一份具体的HTTP请求报文，将报文中各字段数据分别存储在类中的各成员变量中
    HttpResponse http_response;// 创建一个HTTP响应类的对象，用于存储一份具体的HTTP响应报文，将报文中各字段数据分别存储在类中的各成员变量中
    bool stop;    // 这是读取出错的标志位，只要在读取过程中出现了错误，我们就将stop置为ture，然后在后续RecvHttpRequest中做对应的错误处理
    // 这个函数用于从接受缓冲区中读取请求行数据，将数据读到http_request.request_line中
    bool RecvHttpRequestLine()
    {
        // 老版本
            // Util::ReadLine(sock, http_request.request_line);
            // http_request.request_line.resize(http_request.request_line.size()-1);
            // LOG(INFO, http_request.request_line);
        // 新版本
        auto&line = http_request.request_line;
        std::cout << http_request.request_line.size() << std::endl;
        // 我们首先调用工具箱中的read line函数从socket接收缓冲区中。读取一行数据，这行数据就是HTTP请求报文的第一行。也就是请求行的数据。我们把它读到http_request的request_line中
        // 调用ReadLine完成之后，Readline会将接受缓冲区中的一行数据读到 http_request.request_line中，Readline自身返回成功读取的字节数
        // if(Util::ReadLine(sock, line) > 0 && line!="\n"){
        if(Util::ReadLine(sock, line) > 0){
            // std::cout<< int(line[0]) << std::endl;
            line.resize(line.size() - 1);
            LOG(INFO, http_request.request_line);
        }
        else{
            // 走到这里说明读取出错，我们直接将stop置1返回
            stop = true;
            LOG(INFO,"请求行读取出错");
        }
        LOG(INFO,"stop = " + std::to_string(stop));
        return stop;

    }

    // 这个函数用来从接受缓冲区中读取请求报头，将数据读到http_request.request_header中
    bool RecvHttpRequestHeader()
    {
        // 请求报头获取的思路也很简单，主要就是利用请求报头与正文之间隔了一个空行
        // 直接一直ReadLine，直到读到空行为止
        // std::string line;
        // while(line != "\n"){
        //     // 读取之前先把遍历字符串清空
        //     line.clear();
        //     // 从sock对应的接受缓冲区中读取一行数据到line中
        //     Util::ReadLine(sock, line);
        //     // 把刚刚读到的数据push到http_request.request_header中
        //     http_request.request_header.push_back(line);
        //     // 写日志
        //     LOG(INFO, line);
        // }

        // 我现在想把request_header中间的所有换行符都去掉，不想让这些换行符读入request_header中
        std::string line;
        while(1){
            // 读取之前先把遍历字符串清空
            line.clear();
            int line_size=Util::ReadLine(sock, line);
            // 从sock对应的接受缓冲区中读取一行数据到line中
            // if(line_size<=0 || (line=="\n" && http_request.request_header.size()==0)){
                // line_size<0说明读取失败，line_size=0说明对端关闭、连接断开，直接stop置1，退出循环并返回
                // http_request.request_header.size()==0说明是第一次读取
                // 再加上line=="\n"表示第一次读就读到了换行符
            if(line_size<0){
                stop=true;
                LOG(INFO,"请求报头读取出错");
                break;
            }else if(line_size==0){
                stop=true;
                LOG(INFO,"对端连接关闭");
                break;
            }
            if(line=="\n"){
                break;
            }
            // 因为请求报头每一行Line的最后一个字符肯定是\n，所以我把line的大小缩小一字节，不就把最后的换行符去掉了
            line.resize(line.size()-1);
            // 把刚刚读到的数据push到http_request.request_header中
            http_request.request_header.push_back(line);
            // 写日志
            LOG(INFO, line);
        }
        if(line == "\n") http_request.blank = line;
        return stop;
    }
    
    // 这个函数用来从http_request.request_line中读取请求方法、uri和版本号
    void ParseHttpRequestLine()
    {
        auto &line = http_request.request_line;
        std::stringstream ss(line);
        ss >> http_request.method >> http_request.uri >> http_request.version;

        // 我现在有个问题，因为我右面判断请求方法是，都是按照全大写的字符串来比较判断的，比如POST，GET
        // 但是如果客户端给我发http请求报文时，请求方法是大小写混搭的，比如Post，Get
        // 那我后面直接拿==比较的时候，就可能会出问题，因此我想将http_request.method中的字母全部变成大写字母
        auto &method = http_request.method;
        std::transform(method.begin(), method.end(), method.begin(), ::toupper);
        // 其中前两个参数指明参与转化的字符范围，第三个参数指明转化结果的输出位置，第四个参数指明转化方法（此处toupper为大小写转化）
        // 到此为止，http_request.method中的字母就全部变成大写字母了
    }
    
    // 这个函数用来从http_request.request_header中读取各字段的信息，将这些信息从一个个的string（vector<string>）中提取出来，以kv的形式存储在map当中
    void ParseHttpRequestHeader()
    {
        std::string key;
        std::string value;
        for(auto &iter : http_request.request_header)
        {
            // CutString返回值为ture表示切分成功
            if(Util::CutString(iter, key, value, SEP)){
                http_request.header_kv.insert({key, value});
            }
        }
    }
    
    // 这个函数用来判断是否需要读取正文
    bool IsNeedRecvHttpRequestBody()
    {
        // 背景知识，如果method不是POST，而是GET，该请求是没有正文部分的，也就不需要读取
        auto &method = http_request.method;
        std::cout << method << std::endl;
        if(method == "POST"){
            // 现在我们已经确定了请求方法是POST，也就是说需要读取正文，那这次读取读多少字节呢？
            // 这个答案就隐藏在报头的content-length中，所以我们首先就要从报头中找出content-length字段的value值
            // 因为刚刚我们进行报头的解析之后，报头数据。已经存储在map的数据结构中了。所以我们就可以利用map的特性。用insert查找content length字段。
            auto &header_kv = http_request.header_kv;
            auto iter = header_kv.find("Content-Length");
            if(iter != header_kv.end()){
                // 走到这里说明我们找到了consent length字段。那么iter->second就对应着content length字段的具体value值
                LOG(INFO, "Post Method, Content-Length: "+iter->second);
                // 由于iter->second的类型是string，所以我们再对字符串进行处理，将其转化成整形，然后再填入http_request对象的content_length中
                http_request.content_length = atoi(iter->second.c_str());
                return true;
            }
        }
        return false;
    }

    // 这个函数用于将接收缓冲区中的正文部分读取到http_request对象的request_body中
    bool RecvHttpRequestBody()
        {
            if(IsNeedRecvHttpRequestBody()){
                // 你走到这里说明该请求的请求方法是post。需要进行正文的读取
                // 上面这个判断的函数在判断需要读取正文部分的同时，也会将正文的长度读入http_request.content_length中
                // 那么下面的问题就转化成了:已知正文部分的长度，从接收缓冲区中。读取指定长度的数据到http_request.request_body中
                int content_length = http_request.content_length;
                auto &body = http_request.request_body;
                // 我们采用recv一次读一个字节，一共读content_length次
                char ch = 0;
                while(content_length){
                    ssize_t s = recv(sock, &ch, 1, 0);
                    if(s > 0){
                        // 由于recv读取成功时返回实际接收的字节数，连接关闭时返回 0，失败时返回 -1。
                        // 走到这里说明读取成功
                        body.push_back(ch);
                        content_length--;
                    }
                    else{
                        // 由于recv读取成功时返回实际接收的字节数，连接关闭时返回 0，失败时返回 -1。
                        // 走到这里说明链接关闭或者读取失败
                        stop = true;
                        if(s==0){
                            LOG(INFO,"对端连接关闭");
                        }else if(s<0){
                            LOG(INFO,"请求正文读取出错");
                        }
                        break;
                    }
                }
                LOG(INFO, body);// 走到这里说明我们已经完成了对请求报文的正文部分的接收，即已经将请求报文的正文部分从接受缓冲区读到了http_request的request_body中
            }
            return stop;
        }

    // CGI模式下的任务处理
    int ProcessCgi()
    {
        // 表示走到这里时，已经开启了cgi模式
        LOG(INFO, "process cgi mthod!");
        int code=OK;// ProcessCgi()任务返回的状态码
        //父进程数据
        auto& method = http_request.method;
        auto& query_string = http_request.query_string; // /GET
        auto& body_text = http_request.request_body; // /POST
        int content_length = http_request.content_length; // 在前面IsNeedRecvHttpRequestBody函数中，我们已经成功将报头中的content_length字段读到了http_request.content_length
        auto& bin = http_request.path;// 经过前面BuildHttpResponse的处理，http_request.path中存的就是所求资源在服务器中的路径地址（相对于当前运行程序http_server所在目录下的相对地址）
        auto& response_body=http_response.response_body;

        std::string query_string_env;// 用于后面存储环境变量
        std::string method_env;// 用于后面存储环境变量
        std::string content_length_env;
        // 由于父进程也就是httpserver进制直接进行程序替换，会导致后续的服务器程序无法正常运行，因此我们要创建出一个子进程，让子进程帮我去做程序替换
            // 由于我服务器进程要把任务派给子进程，并拿到任务处理的结果，这就要求我们实现父子进程之间的进程通信（如何实现进程通信？）
        // 下面我们就通过管道来实现父子进程之间的进程通信
            // 站在父进程角度，input表示父进程从子进程中读取数据的管道，output表示父进程向子进程中输出数据的管道
        // 一个管道在父子进程中对应两个文件描述符，分别对应管道的读端和写端，我们想要实现管道的单向通信（仅仅只允许父进程从子进程中读取数据），就必须关闭子进程的写端和的父进程的读端

        // 这俩数组用于存储两个管道读写端的文件描述符
        int input[2]; 
        int output[2]; 
        // 下面我们会通过关掉子进程和父进程对这两个管道的部分权限。从而实现两个单向通信的管道

        // 分别创建两个管道，用于实现父进程对子进程中数据的读写
        if (pipe(input) < 0) {
            LOG(ERROR, "pipe input error");
            code=404;
            return code;
        }
        if (pipe(output) < 0) {
            LOG(ERROR, "pipe output error");
            code=404;
            return code;
        }
        // 创建好管道之后，系统也会自动在父进程的文件打开表中为该管道分配两个文件描述符，其中读端的文件描述符会写入input[0]中，写端的文件描述符会写入input[1]中
        
        // 从头到尾都只有一个进程，就是httpserver！
        pid_t pid = fork();
        if (pid == 0) { //走到这里说明执行后面代码的是child进程
            close(input[0]); // 我们只允许父进程，而不允许子进程从Input中读数据，
            close(output[1]);// 我们只允许父进程，而不允许子进程从output中写数据/输出数据

            // 之前我们是在这里重定向的，后来我们把重定向操作放到紧挨着execl的地方了，请问这是为什么？
                // 原因就是因为后续的LOG日志，原本是打印在服务器终端的屏幕上的，如果在这里重定向之后，就改成打印到管道文件中了

            // 将请求方法保存到环境变量中，这样进程替换之后也能访问到
            method_env = "METHOD=";
            method_env += method;
            putenv((char*)method_env.c_str());
              
            // 在程序替换之前，我们还要完成一件事情，那就是子进程要获得请求报文中上传的信息
                // 然后我们就要来分析一下请求的类型了
                    // 如果是POST请求，则上传的信息就在正文中，前面已经读取到http_request.request_body中了，我们就在父进程中直接通过write函数，将正文信息写到管道文件中，然后子进程直接调用read函数，就可以将信息从管道中读出来了
                    // 如果是GET请求，则我们前面已经把上传的信息存入到http_request.query_string中了，我们除了可以像上面一样，将信息通过管道传入，还可以采用另一种方法，也就是下面的方法，设置环境变量
                        // 子进程进行进程替换之后，替换语句前面创建的环境变量同样不会被销毁，进程替换之后依然可以访问到（因为进程替换替换的是代码区和数据区，而环境变量存在内核区）
                // 需要注意的是，基于上面的方案，子进程要根据请求方法的不同，采用不同的信息接受方式（管道or环境变量），所以子进程要实现得知请求的种类
                // 现在问题又来了，子进程如何得知http请求的请求方式呢？（答案是通过环境变量传递）
 
            if(method == "GET"){
                // 通过环境变量，将GET请求报文中上传上来的参数传递给替换后的新进程
                query_string_env = "QUERY_STRING=";
                query_string_env += query_string;
                putenv((char*)query_string_env.c_str());
                LOG(WARNING, "Get Method, Add Query_String Env");
            } else if (method=="POST"){
                // 通过环境变量，将POST请求报头中content_length传递给替换后的新进程，然后新进程再从正文中读取对应字段的数据，完成参数的读取
                content_length_env = "CONTENT_LENGTH=";
                content_length_env += std::to_string(content_length);
                putenv((char*)content_length_env.c_str());
                LOG(WARNING, "POST Method, Add Content_Length Env");
            }

            // 如果上面的if语句没有执行，说明此时的method是POST，因为method既不是POST也不是GET的报文，我们在BuildHttpResponse中已经处理过了
            // 对于method=POST的情况，我们

            // 对子进程进行输出重定向
            //站在子进程角度
            //input[1]: 写出 ———— fd=1 -> input[1]
            //output[0]: 读入 ———— fd=0 -> output[0]
            dup2(output[0], 0);
            dup2(input[1], 1);
            // 上面俩工作实际上就是让子进程的文件打开表中fd[0]中存储
            // 此后fd_array[1]中的指针指向的就是我们input管道的写端了，fd_array[0]中的指针指向的就是我们outPut管道的读端了
            
            //exec*，构造完单向管道之后，我们就让子进程去执行路径中指向的可执行程序了
             execl(bin.c_str(), bin.c_str(), nullptr);
            // 进行程序替换之后，前面创建好的管道文件会被销毁吗？
                // 程序替换只替换代码和数据，并不替换内核进程相关的数据结构，包括：文件描述符表。换句话来说就是，当前子进程的代码区和数据区都被替换了，但是子进程前面执行过程中创建的进程控制块、还有打开的文件并不会被替换，依然是存在的，只是子进程代码区和数据区被替换之后，子进程找不到它们了
            // 现在子进程进行进程替换之后，已经不知道前面创建好的管道的文件描述符了，那子进程现在应该如何对前面的管道进行读写操作呢
                // 我们的解决方法是，在进程替换之前，对该子进程进行输出重定向的工作（调用dup2）
                // 输入输出重定向之后，fd=0就指向管道文件input的读端，fd=1就指向管道文件output的写端
            
            
            // 因为正常情况下，执行execl之后，子线程就去执行新程序去了，execl后面的代码是不会执行的
            // 所以如果走到了这，就说明execl调用失败了
            exit(1);
        }
        else if (pid < 0) { //error
            LOG(ERROR, "fork error!");
            return 404;
        }
        else { //parent
            close(input[1]); // 我们只允许子进程，而不允许父进程从Input中写数据
            close(output[0]); // 我们只允许子进程，而不允许父进程从output中读数据
            if(method == "POST"){
                // 下面父进程要读取出POST请求报文的正文信息，并将这个信息通过管道传递给子进程
                    // 前面我们已经把http请求报文中的正文信息读取到了http_request.request_body中
                    // 那么现在我们就要将正文信息写入output管道文件中
                const char *start = body_text.c_str(); // 正文起始位置的指针
                int total = 0; // 记录当前已经读取的数据
                int size = 0;  // 记录每次读取的数据大小
                while(total < content_length && (size = write(output[1], start+total, body_text.size()-total)) > 0){
                    // 上面的write语句的意思是，从start+total处开始读，读取body_text.size()-total个字节，将刚刚读取到的数据写入管道文件output的写端
                    total += size;
                }
            }
            // 上面，父进程已经把客户端上传的参数传递给了子进程（如果GET方法，子进程就从环境变量中直接拿，如果是POST方法，上面刚刚把正文写入管道，子进程从output管道里读即可）
            // 现在父进程要通过管道获取子进程任务执行的结果，也就是从input管道文件中读取信息，将结果写入响应报文的正文中

            waitpid(pid, nullptr, 0); // 父进程调用waitpid，等待子进程的退出。第二个参数status设置为null，表示父进程不关系子进程的退出状态，第三个参数flag设置为0，表示默认阻塞等待
            
            // 子进程退出前，会将任务处理的结果写入input管道中
            // 等到子进程退出之后，父进程阻塞状态解除，继续执行waitpid之后的代码
            // 下面父进程就要从input管道中读出子进程任务处理的结果
            char ch = 0;
            while(read(input[0], &ch, 1) > 0){
                response_body.push_back(ch);
            }

            int status = 0;
            pid_t ret = waitpid(pid, &status, 0);

            if(ret == pid){
                if(WIFEXITED(status)){// WIFEXITED(status): 若为真，表示子进程正常退出，若为0表示子进程异常退出。
                    // 如果WIFEXITED(status)为真，则说明子进程正常退出，我们可以通过WEXITSTATUS(status)来获得子进程退出时的状态码
                    if(WEXITSTATUS(status) == 0){// 说明正常终止的子进程返回的退出状态码为 0 ，表示子进程成功执行完毕 
                        // 
                        code = OK;
                    }else{
                        // 走到这里说明正常终止的子进程返回的退出状态码不是0，说明子进程并没有正常执行完毕
                        LOG(INFO,"子进程正常终止, 但是返回的退出状态码(status第9到16位)不是0, 说明子进程并没有正常执行完毕");
                        code = BAD_REQUEST;
                    }
                } 
                else{// 走到这里说明WIFEXITED(status)=false，表示子进程异常退出
                    LOG(INFO,"status低七位不是全0, 说明子进程被信号杀了");
                    code = SERVER_ERROR;
                }
            }else{
                // 走到这里表示ret为-1，表示waitpid调用出错
                // 错误原因可能是传入waitpid的子进程pid无效，也可能是父进程没有权限等待第一个参数pid指定的子进程
            }
            // 父进程执行完毕之后，关闭管道文件
            close(input[0]);
            close(output[1]);
        }
        return OK;
    }

    // 非CGI模式下的任务处理，就是简单网页返回，返回静态网页
        // 注意，我们前面仅仅向http_response对象中填充了部分属性信息
        // 现在我们就要根据这些信息构建请求行和响应报头行（正文行就不用构建了，我们直接调用senfile把资源直接发出去）
    // 这个是不考虑差错处理的代码实现，考虑之后就废弃不再使用了
    int ProcessNonCgi_OLD(int size){
        // 构建http响应报文
        http_response.fd = open(http_request.path.c_str(), O_RDONLY);
        if(http_response.fd>=0){// 只有资源文件打开成功，才能继续往下构建响应报文
        // 1.构建状态行，众所周知，状态行从左到右依次是：http版本号，状态码，以及状态码描述，下面我们就来一个个构建
            http_response.status_line = HTTP_VERSION; // http版本号
            http_response.status_line += " ";
            http_response.status_line += std::to_string(http_response.status_code); // 状态码
            http_response.status_line += " ";
            http_response.status_line += Code2Desc(http_response.status_code); // 状态码描述
            http_response.status_line += LINE_END; // 加一个换行符
            http_response.size=size; // 这个传入的size是http请求报文请求的那份资源的文件大小

        // 2.构建响应报头行，我们目前仅仅只写两个必要的字段，即正文资源类型和正文资源长度，后面你要是想写可以再自己加
            std::string header_line = "Content-Type: ";
            header_line += Suffix2Desc(http_request.suffix); //实际的资源类型
            header_line += LINE_END; // 换行符
            http_response.response_header.push_back(header_line); // 将这行key value添加到响应报头response_header中

            header_line = "Content-Length: ";
            header_line += std::to_string(size); // 请求资源的文件大小
            header_line += LINE_END;
            http_response.response_header.push_back(header_line); // 将这行key value添加到响应报头response_header中

            return OK;
        }else{
            return NOT_FOUND;
        }
    }

    // 这个是考虑差错处理之后的代码实现
    int ProcessNonCgi()
    {
        // 有了HandlerError函数之后，我们处理静态资源请求的函数ProcessNonCgi()就只需要负责打开对应路径下的资源文件，并将文件描述符存入http_response.fd即可
        // 我们会在最后的SendHttpResponse中用到这个打开的文件，并关闭它
        http_response.fd = open(http_request.path.c_str(), O_RDONLY);
        if(http_response.fd >= 0){
            return OK;
        }
        return 404;
    }
    
    // 专门用来构建状态码为404的响应报头，引入HandlerError之后就可以被HandlerError(PAGE_404)替代了
    void HandlerNotFound(){
        // 走到这里说明返回状态码是404，也就是说我们按照用户给的路径去找，发现没有找到他要的文件
        // 既然无法返回用户要的文件，我们就用户返回一个404.html文件
        http_response.fd = open(PAGE_404, O_RDONLY); 
        // 为啥要打开这个文件？我们会在最后SendHttpResponse的sendfile中用到这个打开的文件（将这个文件传到sock的发送缓冲区中），并关闭它
        if(http_response.fd > 0){// 如果fd>0，说明这个PAGE_404指向的404.html文件被成功打开
            struct stat st;
            stat(PAGE_404, &st); // 将st与PAGE_404指向的404.html文件绑定起来
            std::string line; // 用于存储一行响应报头，写满一行插一行
            line = "Content-Type: text/html"; // 404.html文件类型是text/html
            line += LINE_END;
            http_response.response_header.push_back(line);
            line = "Content-Length: "; 
            line += std::to_string(st.st_size);// 这里的st.st_size是PAGE_404指向的404.html文件的文件大小
            line += LINE_END; 
            http_response.response_header.push_back(line);
            // 到此为止，我们响应报头也构建完毕了
        }
    }

    // 我们构建一个比HandlerNotFound更泛用的处理函数，用于构建所有的非正常状态码对应的响应报文
    void HandlerError(std::string page) // page是非正常状态码对应的处理文件，比如404对应的page就是404.html
    {
        // 很有可能，我们前面判断cgi=ture之后，发现找不到对应的页面，所以要进行差错请求处理
        http_request.cgi = false; // 这个置位的作用主要是为了最后我们发送响应报文的正文部分时，按照非cgi模式直接返回对应404.html
        
        // 文件名转化成相对路径 这个操作我们在BuildHttpResponseHelper中干，这里就不用干了
            // std::string _page(page);
            // page=WEB_ROOT;
            // page+= "/" +_page;
        //要给用户返回对应的404页面
        LOG(INFO,"error page = "+page);
        http_response.fd = open(page.c_str(), O_RDONLY);
        
        if(http_response.fd > 0){
            struct stat st;
            stat(page.c_str(), &st);
            http_response.size=st.st_size; // 将对应错误处理文件（比如404.html）的大小返回给http_response.size，作为响应报文正文部分的长度

            std::string line;
            line = "Content-Type: text/html";
            line += LINE_END;
            http_response.response_header.push_back(line);

            line = "Content-Length: ";
            line += std::to_string(st.st_size);
            line += LINE_END;
            http_response.response_header.push_back(line);
        }
    }

    void BuildOkResponse()
    {
        // 状态行前面函数已经写好了，我们这个函数的任务是将构建响应报文的响应报头，写入response_header中
        std::string line;// 构建

        line = "Content-Type: ";
        line += Suffix2Desc(http_request.suffix);// 根据请求资源的后缀，返回对应的文件类型
        line += LINE_END;
        http_response.response_header.push_back(line);

        line = "Content-Length: ";
        if(http_request.cgi){
            // cgi函数会把对应程序执行后的结果写入http_response.response_body中
            // 此时响应报文正文长度可以直接通过response_body.size()获取
            line += std::to_string(http_response.response_body.size());
        }
        else{
            // 非cgi模式下（静态网页请求），我们会将请求的资源写入响应报文的正文
            // 因此我们响应报文正文的长度，就是请求资源的文件大小
            line += std::to_string(http_request.size); // Get
        }
        line += LINE_END;
        http_response.response_header.push_back(line);
    }
    
    // 这个函数用于构建响应报文的状态行和响应报头
    void BuildHttpResponseHelper(){
        // 获取响应报文的状态码
        auto& code=http_response.status_code;
        LOG(INFO, "状态码："+std::to_string(code));

        // 构建状态行，将构建好的状态行填入http_response.status_line中
        auto &status_line = http_response.status_line;
        status_line += HTTP_VERSION; 
        status_line += " ";
        status_line += std::to_string(code); // 由于code的类型是int，所以你添加之前要先将其转成string
        status_line += " ";
        status_line += Code2Desc(code); // Code2Desc(code)返回对状态码code的状态码描述
        status_line += LINE_END;

        // 方便将后面的文件名转化成相对路径
        std::string path=WEB_ROOT;
        path+="/";
        // 构建响应报头和响应正文
        switch(code){
            case 200:
                BuildOkResponse();
                break;
            case 404:
                path+=PAGE_404;
                HandlerError(path); 
                break;
            case 400:
                path+=PAGE_400;
                HandlerError(path); 
                break;
            case 500:
                path+=PAGE_500;
                HandlerError(path); 
                break;
            default:
                break;
        }
    }
public:
    // 构造函数
    EndPoint(int _sock):sock(_sock){}
    
    // 这个函数用来读取http请求报文的请求行和报头
    void RecvHttpRequest()
    {
        // RecvHttpRequestLine与RecvHttpRequestHeader两个函数返回的结果分别表示在这两次读取过程中，有没有出现读取失败的情况
        // 如果返回值为false，说明中间读取非常顺利
        if((!RecvHttpRequestLine()) && (!RecvHttpRequestHeader())){ 
            // 如果这俩函数的读取都非常顺利，我们就进一步去解析它们读上来的数据
            ParseHttpRequestLine();
            ParseHttpRequestHeader();
            // 这俩函数执行完之后，我们就将一份http报文的请求行和请求报头从接收缓冲区读到了http_request对象的requestline和requestheader中
            RecvHttpRequestBody();
            // 这个函数再执行完之后，如果这个请求有正文部分，我们就把这个请求的正文部分读到了http_request.body中
        }
        
    }

    // 这个函数用来解析http请求报文，将一份http请求报文对应的一大长串字符串信息转化成http_request对象中的各种属性信息
    // 我们后面把ParseHttpRequest合并到RecvHttpRequest里面去了，所以这个函数后面就不调用了
    void ParseHttpRequest()
    {
        ParseHttpRequestLine();
        ParseHttpRequestHeader();
        RecvHttpRequestBody();
    }

    // 这个函数用于构建http响应报文（填充HttpResponse对象的各字段）
    void BuildHttpResponse()
    {
        // 放在最前头定义主要是怕goto语句报错，因为在 C 和 C++ 里，goto 语句不能跳过变量的初始化，不然就会引发编译错误。
        // 这几个变量具体的含义会在对应位置给出注释
        std::string _path;
        int size;
        std::size_t found;
    // 在正式开始填充之前，其实我们还有很多工作要做
        // 首先我们要知道，这个http报文要向服务器请求什么资源？
        // http报文中请求的资源在服务器中的什么位置？
        auto &code = http_response.status_code;

        if(http_request.method != "GET" && http_request.method != "POST"){
            //走到这里说明是非法请求，我们将code置为BAD_REQUEST，直接向客户端返回400.html
            LOG(WARNING, "method is not right");
            code = BAD_REQUEST;
            goto END;
        }
        else if(http_request.method == "GET"){
            // 走到这里说明此时的请求方法是GET，下面我们要获得这个GET请求的资源请求路径
                // 首先我们要判断这个GET请求是否带参，因为带参与不带参两种情况下资源请求路径的获得方式是不一样的
                    // http请求报文的uri由url+客户端上传字段组合而成，这两部分之间用?来链接
                    // 如果不带参，那么uri就是url，如果带参，那么uri中?之前的部分就是url
            size_t pos = http_request.uri.find('?');// 那么我们首先调用cutstring以?为界限对其进行一个分割
            if(pos != std::string::npos){
                    // 将前面的url写入http_request.path中
                    // 将后面的上传字段写入http_request.query_string中
                Util::CutString(http_request.uri, http_request.path, http_request.query_string, "?");
                http_request.cgi=true;// GET方法带参，后续process处理要用cgi机制
            }
            else{
                //  走到这说明GET请求不带参，此时的uri就是url
                http_request.path = http_request.uri;
            }
            
        }else if(http_request.method == "POST"){
            // POST方法提交的参数蕴藏在请求报文的正文部分，后续我们在CGI函数中处理
            // 因此POST请求报文中的uri就是url
            http_request.cgi=true;
            http_request.path=http_request.uri;
            // 注意，这一行非常重要，如果没有这一行，你后续执行的时候，你的path就是空的，
            // 即你后续对POST请求做处理时，你会发现path并没有指向一个特定的资源，就是因为在这里没有将请求行中的uri写入path
        }else{
            // do nothing
        }

        // 现在我们已经拿到了资源的路径，下一步是将这个拿到的地址（相对默认资源请求路径的地址）转化成相对于http_server进程所在目录下的地址
            // 走到这里我们已经成功地将报文请求行中的url存入了http_request.path中
            // 注意：这时候http_request.path拿到的路径是相对路径，/并不代表服务器的根目录，而是代表我们服务器的默认资源请求路径
            // 然后我们下一步想让http_request.path中的路径变成服务器根目录下的绝对路径，其实就是要把默认资源请求路径插入到相对路径的头部（如何实现？）
                // 为了方便，我们用先宏WEB_ROOT记录下服务器的默认资源请求路径
                // 然后用中间变量_path记录相对路径
                // 俩再一加就是绝对路径
        _path = http_request.path;
        http_request.path=WEB_ROOT;
        http_request.path+=_path;

        // 现在我们还要处理一种特殊情况，就是如果 http_request.path以/结尾，应该如何处理？
            // 处理方法为：让http_request.path指向默认资源目录
        if (http_request.path[http_request.path.size() - 1] == '/') {
            http_request.path += HOME_PAGE;
        }

        // 到目前为止，我们已经从http请求报文的uri中读取了资源请求路径，并将其写入了http_request.path中
        std::cout <<"http_request.path: "<< http_request.path << std::endl;
        // 下面我们的任务就是，确认http_request.path中的资源路径是否有效，即这个地址指向的位置处到底有没有资源
            // 如何判定http_request.path指向的文件是否存在呢？
                // 我们的解决方法是采用stat函数，读取http_request.path指向文件的属性信息
                // 如果返回值为0，说明成功读取，那么这个文件就是真实存在的
                // 如果返回值为-1，说明调用失败，文件不存在
        struct stat st;
        if (stat(http_request.path.c_str(), &st) == 0) {
            // stat函数的作用是，将http_request.path指向文件的状态信息都记在stat型对象st中
            // 走到这里说明资源是存在的，但是这个资源文件可能是一个目录文件，也可能是一个可执行文件，对于这样的文件我们就需要做一些特殊处理
            if (S_ISDIR(st.st_mode)) {
                // S_ISDIR返回值为ture，说明请求的资源是一个目录文件，这是不被允许的，因为目录文件并不是一份具体的资源，我们需要做一下相关处理
                // 由于我们前面对path结尾的/做过处理，所以此时即使path指向目录文件，他结尾也不会是/
                // 我们人为规定，任何目录文件下，都有一个默认的资源文件，比如说都有一个index.html，我们用宏HOME_PAGE来表示这个默认文件
                http_request.path += "/";
                http_request.path += HOME_PAGE;
                // 由于此时path指向的文件发生了改变，那么st就需要重新进行绑定，这样才能得到真正的资源属性信息
                stat(http_request.path.c_str(), &st);
            }
            else if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)) {
                // 走到这里说明http_request.path指向的文件存在，并且是一个可执行文件
                // 此时也要用cgi机制
                http_request.cgi=true;
            }
            http_request.size =st.st_size; 
            http_response.size =st.st_size; 
            // 将st与路径指向的资源绑定之后，我们可以通过st.size获得该请求资源的大小
            // 我们可以将这个资源大小存入http_response.size中，这样后续函数想用它时就不用专门传这个参数了
        } else {
            //说明资源是不存在的
            std::string info = http_request.path;
            info += " Not Found!";
            LOG(WARNING, info);
            code = NOT_FOUND;
            goto END;
        }

    // 走到这里说明当前http_request.path指向的文件一定是存在的，并且这个文件既不是目录文件，也不是可执行文件（这俩前面都做过特殊处理）
    // 那么现在我们就要通过该文件的后缀，来判断请求资源的类型
        // 接下来我们的工作就是要获得资源的文件名后缀（如何获得？）
        found = http_request.path.rfind(".");
            // 这行代码的功能是从 http_request.path 字符串的末尾开始，反向查找字符 . 首次出现的位置。
            // 若找到了则把该位置的下标赋给变量 found；若未找到，found 会被赋值为 std::string::npos。
        if(found == std::string::npos){
            // 如果没找到，资源后缀默认是.html
            http_request.suffix = ".html";
        }
        else{
            // 如果找到了，我们就把found下标朝后的子字符串截取出来
            http_request.suffix = http_request.path.substr(found);
                // substr的形式
                // std::string substr( size_type pos = 0, size_type len = npos ) const;
                // 作用是提取从 pos 位置开始，长度为 len 的子字符串
                // 如果第二个参数缺省，即Len= npos ，则表示提取从 pos 位置开始直到字符串末尾的所有字符。
            
        }
        // 到此，我们就把请求资源的文件名后缀写到了http_request对象的suffix中
        // 至于后续的判断类型的工作，我们放在Process函数中完成

        // 判断是否需要用cgi机制来响应
        if (http_request.cgi) {
            ProcessCgi(); // 执行uri指向的目标程序，将程序执行完毕之后的结果写到http_response.response_body中
            
        } else {
            code=ProcessNonCgi(); // ProcessNonCgi的工作很简单，就是简单的网页返回，返回静态网页 
            // 这个code是http_response.status_code的别名
            // 这个size是http请求报文请求的那份资源（比如index.html文件）的文件大小
            // 也就是说，这句代码的作用是，ProcessNonCgi(size)返回响应之后的结果，保存在http_response.status_code中
        }
END:
            BuildHttpResponseHelper();
            // 调用完这个函数之后，我们就根据状态码code将其对应的状态行、响应报头和正文都写入了http_response的各个对应字段中
    }
    
    // 在执行完BuildHttpResponse之后，我已经将要发送的响应报文的各种信息都填进了http_response对象的各属性中
    // SendHttpResponse就是在此基础上，调用send函数，将响应报文发送给客户端
    void SendHttpResponse()
    {
        // 发送状态行
        send(sock, http_response.status_line.c_str(), http_response.status_line.size(), 0);
        // 发送响应报头
        for (auto iter : http_response.response_header) {
            send(sock, iter.c_str(), iter.size(), 0);
        }
        // 发送空行
        send(sock, http_response.blank.c_str(), http_response.blank.size(), 0);

        // 发送正文
        if(http_request.cgi){
            auto &response_body = http_response.response_body;
            size_t size = 0;  // 这次发送了多少字节
            size_t total = 0; // 总共发送了多少字节
            const char *start = response_body.c_str(); // 
            while( total < response_body.size() && (size = send(sock, start + total, response_body.size() - total, 0)) > 0)
            {
                total += size;
            }
        }else{
            LOG(INFO,"http_response.fd ............." + std::to_string(http_response.fd));
            LOG(INFO,"http_response.size ..........." + std::to_string(http_response.size));
            // 由于在非cgi模式下，响应报文的正文就是前面客户端向服务器请求的资源，资源存放在服务器的磁盘中
            // 因此我们可以通过sendfile简化传输路线，提高传输效率
            sendfile(sock, http_response.fd, nullptr, http_response.size);
            close(http_response.fd);
        }
        
    }
    
    bool Stop(){
        return stop;
    }
    ~EndPoint()
    {
        close(sock);
    }
};

// Entrance Version 2 实现从缓冲区中读取请求，并对相应进行处理

// class Entrance{
class CallBack{
public:
    CallBack(){}
    ~CallBack(){}
    void operator()(int sock)
    {
        HandlerRequest(sock);
    }
    static void* HandlerRequest(int sock)
    {
        // _sock是前面accept返回的sock
        LOG(INFO,"Hander Request Begin");
        // int sock = *(int*)_sock;
        // delete (int*)_sock;
        // 之所以要上面这种处理，本质上就是与Httpserver.hpp中pthread_create(&tid, nullptr, Entrance::HandlerRequest, _sock);匹配对应
        // 顺利地解决生命周期导致的传参错误问题
#ifdef DEBUG // 如果我们想编译这段代码，我们应该在gcc指令的最后加上 -DDBUG
        //For Test
        char buffer[4096];
        recv(sock, buffer, sizeof(buffer), 0);
        std::cout << "----------------begin-----------------" << std::endl;
        std::cout << buffer << std::endl;
        std::cout << "----------------end-------------------" << std::endl;
#else
        EndPoint *ep = new EndPoint(sock);
        ep->RecvHttpRequest(); // 接收http请求报文
        // ep->ParseHttpRequest(); // 解析http请求报文
            // 我们后面把ParseHttpRequest合并到RecvHttpRequest里面去了，所以这个函数后面就不调用了
        if(!ep->Stop()){
            // 如果前面RecvHttpRequest执行完毕之后，EndPoint::stop还没有被置1，说明前面一切顺利
            LOG(INFO,"Recv No Error, Begin Build And Send");
            // 下面我们就来构建和发送响应报文
            ep->BuildHttpResponse(); // 构建http响应报文
            ep->SendHttpResponse(); // 发送http响应报文
        }else{
            LOG(WARNING, "Recv Error, Stop Build And Send");
        }
        // 如果前面RecvHttpRequest执行完毕之后，EndPoint::stop是true，说明前面没能成功把数据读上来，那我们就不构建响应报文了，直接退出
        delete ep; // 调用析构函数，关闭通信套接字sock，断开连接
#endif 
        LOG(INFO,"Hander Request End");
        return nullptr;
    }
};

// Entrance Version 1 仅仅实现从缓冲区中读取请求，然后将请求内容打印出来
// class Entrance{
//     public:
//         // 这个函数是具体处理请求的函数，传入的参数是客户端对应的通信套接字地址
//         static void *HandlerRequest(void *_sock)
//         {
//             int sock = *(int*)_sock;
//             delete (int*)_sock;
//             // 因为_sock指向的这块内存是在主线程里用 new 分配的（int *_sock = new int(sock);），且在新线程里已经不再需要它来存储套接字描述符了，所以需要手动释放以避免内存泄漏。 = new int(sock);
//             std::cout << "get a new link ... : " << sock << std::endl;

// #ifndef DEBUG
// #define DEBUG
//             1.读取请求
//                 这是一份测试代码，因为buffer的大小有限，并不能保证把一个http报文读完，这就很有可能会出现数据粘包的问题
//                 在用户缓冲区创建
//                 调用recv函数，将sock对应的对端发来的数据从内核缓冲区读到buffer中
//                 char buffer[4096];
//                 recv(sock, buffer, sizeof(buffer), 0);
//                 std::cout << "----------------begin----------------" << std::endl;
//                 std::cout << buffer << std::endl;
//                 std::cout << "----------------end----------------" << std::endl;
// #endif
//                 std::string line;
//                 // 这个函数的作用是从sock对应的接受缓冲区中，读取一行数据到line中
//                 Util::ReadLine(sock, line);
//                 std::cout <<line<<std::endl;
//             close(sock);
//             return nullptr;
//         }
// };